On this page

Skip to content

A Brief Discussion on C# Property Syntactic Sugar and NRT Mechanism Evolution

TLDR

  • Property initializers and expression-bodied properties differ in their lifecycle; the former executes only once during object initialization, while the latter executes every time it is called. Misuse can lead to logical errors.
  • The field keyword introduced in C# 14 resolves the pain point of having to revert to manual backing fields for simple logic (such as Trim()) in auto-properties.
  • The NRT (Nullable Reference Types) mechanism must be paired with <WarningsAsErrors>nullable</WarningsAsErrors> to achieve mandatory enforcement.
  • required and init syntax solve the issue where DTOs had to be written with meaningless null! default values under the NRT mechanism.
  • Using [SetsRequiredMembers] resolves compilation conflicts between parameterized constructors and required properties.

C# Property Syntax Evolution and Pitfalls

The evolution of C# properties aims to simplify definitions, but different syntaxes have significant differences in execution timing. If not clarified, they can easily trigger bugs.

Difference Between Property Initializers and Expression-bodied Properties

When you might encounter this issue: Confusing "static caching" and "dynamic calculation" syntax when defining read-only properties or setting initial values.

  • public string Name => "Default" (Expression-bodied): Re-executes every time it is called.
  • public string Name { get; } = "Default" (Property Initializer): Executes only once when the object is instantiated (new).

WARNING

Disaster Scenario: If you use public Guid OrderId => Guid.NewGuid();, a brand new Guid will be generated every time the property is read, causing serialization or log tracking to fail. You should use public Guid CorrectOrderId { get; } = Guid.NewGuid(); instead to ensure a constant state.

Simplifying Property Logic with the field Keyword

When you might encounter this issue: When you need to add simple logic (such as Trim() or NotifyPropertyChanged) to an auto-property but do not want to write a verbose backing field.

The field keyword introduced in C# 14 allows direct access to the implicit field generated by the compiler:

csharp
public class User {
    public string Name { 
        get;
        set => field = value.Trim();
    }
}

TIP

It is recommended to place logic processing in the set accessor to avoid the overhead of frequent calls in get and to reduce potential side effects when frameworks like Entity Framework Core access the backing field directly.

NRT (Nullable Reference Types) and Mechanism Completion

The core of NRT lies in explicitly declaring the nullability of reference types using ?. To enforce this contract, you should set the following in your project file:

xml
<WarningsAsErrors>nullable</WarningsAsErrors>

Solving the Initialization Dilemma for DTOs under NRT

When you might encounter this issue: During the C# 8.0 to 10.0 era, non-null properties had to be initialized, forcing DTOs to be written with null! or empty strings to bypass the compiler, resulting in code noise.

Through the init and required keywords, you can ensure that properties are immutable after initialization and force the caller to provide a value, eliminating the need for meaningless default values:

csharp
public class UserDto {
    public required string UserName { get; init; }
}

// The caller must provide a value, otherwise compilation fails
UserDto dto = new() { UserName = "Alice" };

Handling Conflicts Between Constructors and required

When you might encounter this issue: When a class defines both a parameterized constructor and required properties, the compiler will issue a warning because it cannot perform initialization via { }.

Using the [SetsRequiredMembers] attribute informs the compiler that the constructor has completed the assignment of all required members:

csharp
using System.Diagnostics.CodeAnalysis;

public class User {
    public required string UserName { get; init; }

    [SetsRequiredMembers]
    public User(string userName) {
        UserName = userName; 
    }
}

Application of required in Web API

In ASP.NET Core's [FromBody] serialization, if a property is marked as required, the system will automatically throw a JsonException if the frontend fails to pass that field. This helps clearly distinguish the semantic difference between "passing a default value" and "not passing a field at all."

Change Log

  • 2026-03-30 Initial version created.